package net.towee.client.managers.impl;

import java.util.LinkedList;
import java.util.Map;

import net.towee.client.event.core.DiscussionMessageReceivedEvent;
import net.towee.client.managers.AccountHelper;
import net.towee.client.managers.DiscussionManager;
import net.towee.model.account.AccountPermission;
import net.towee.model.discussion.Discussion;
import net.towee.model.discussion.DiscussionSubject;
import net.towee.model.discussion.Message;
import net.towee.model.discussion.impl.DiscussionImpl;
import net.towee.model.utils.Pair;
import net.towee.server.rpc.services.ToweeServiceAsync;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.MapMaker;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;

public class DiscussionManagerImpl implements DiscussionManager {
	private final Map<String, Discussion> discussionCache;

	private final ToweeServiceAsync service;
	private final HandlerManager eventBus;
	private final AccountHelper accountHelper;

	@Inject
	public DiscussionManagerImpl(ToweeServiceAsync service, HandlerManager eventBus,
			AccountHelper accountHelper) {
		this.service = service;
		this.eventBus = eventBus;
		this.accountHelper = accountHelper;

		this.discussionCache = new MapMaker().maximumSize(200).makeMap();
		createPushHandlers();
	}

	private void createPushHandlers() {
		service.onDiscussionMessageUpdate(null, null, new AsyncCallback<Pair<String, Message>>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(Pair<String, Message> result) {
				// Put to cache if discussion exists
				Discussion disc = discussionCache.get(result.getFirst());

				// Add message to discussion
				if (disc != null)
					discussionCache.get(result.getFirst()).addMessage(result.getSecond());

				// Fire an event
				eventBus.fireEvent(new DiscussionMessageReceivedEvent(result.getSecond(), result
						.getFirst(), false));
			}
		});
	}

	@Override
	public void getDiscussion(final String id, DiscussionSubject subject, int offset, int limit, final AsyncCallback<Discussion> handler) {
		// For chache discussion
		final Discussion disc = discussionCache.get(id);

		final int neededOffset = offset;
		final int neededLimit = limit;

		if (disc != null) {
			Preconditions.checkArgument(offset < disc.getCount(), "discussion out of range");
			limit = (offset + limit > disc.getCount()) ? disc.getCount() - offset : limit;

			// Get cached discussion
			int mesCount = disc.getMessagesList().size();
			int diff = 0;

			// Check offset/limit intersection between number of laready
			// loaded messages
			if (mesCount <= offset) {
				limit += offset - mesCount;
				offset = mesCount;
			} else if (mesCount < offset + limit) {
				diff = mesCount - offset;

				offset += diff;
				limit -= diff;
			} else {
				// Create new discussion object for storing range of messages
				Discussion result = new DiscussionImpl();
				result.setMessages(disc.getMessagesList());

				// Set messages range
				setRangeOfMessages(result, offset, limit);

				// Invoke handler
				handler.onSuccess(result);

				// Don't get anything from server
				return;
			}
		}

		// Get discussion with needed offset and limit
		service.getDiscussion(id, subject, offset, limit, new AsyncCallback<Discussion>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Discussion result) {
				Preconditions.checkNotNull(result, "null discussion");

				// If discussion id in cache
				// add returned messages
				if (disc != null) {
					// add messages
					disc.addMessages(false, result.getMessagesList());

					// set messages in result discussion
					result.setMessages(disc.getMessagesList());
				} else
					putDiscussionToCache(id, result);

				// Get needed range of messages
				setRangeOfMessages(result, neededOffset, neededLimit);

				// Invoke handler with needed messages range
				handler.onSuccess(result);
			}
		});
	}

	private void setRangeOfMessages(Discussion disc, int offset, int limit) {
		int start = disc.getMessagesList().size() - offset - limit;
		start = (start < 0) ? 0 : start;
		int end = start + limit;
		end = (end >= disc.getMessagesList().size()) ? disc.getMessagesList().size() : end;

		disc.setMessages(new LinkedList<Message>(disc.getMessagesList().subList(start, end)));
	}

	@Override
	public void addMessage(final Discussion disc, final String id, DiscussionSubject subject, final Message mess, final AsyncCallback<Message> handler) {
		/*if (subject.equals( DiscussionSubject.DOCUMENT ))
			Preconditions.checkArgument(accountHelper.hasPermissions(disc.getSubject().toDocument()
					.getGroup().getId(), AccountPermission.CAN_ADD_MESSAGES), "no permission");*/

		Preconditions.checkNotNull(id, "null discussion subject id");
		Preconditions.checkNotNull(mess, "null message");
		Preconditions.checkNotNull(subject, "null discussion subject");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(mess.getText()), "null message text");

		// Set author as current logged in user
		mess.setAuthor(accountHelper.getAccount().getAccountIdentity());

		// Add message
		service.addDiscussionMessage(id, subject, mess, new AsyncCallback<String>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(String messId) {
				Preconditions.checkArgument(!Strings.isNullOrEmpty(messId), "fail add message");

				// Set message id
				mess.setId(messId);
				
				// Put to cache
				disc.addMessage(mess);

				// Fire an event and invoke handler
				eventBus.fireEvent(new DiscussionMessageReceivedEvent(mess, id, true));
				handler.onSuccess(mess);
			}
		});
	}

	@Override
	public void changeMessage(final Discussion disc, final String id,
			DiscussionSubject subject, final Message mess, final AsyncCallback<Boolean> handler) {
		/*if (disc.getSubject().toDocument() != null)
			Preconditions.checkArgument(
					mess.getAuthor().equals(accountHelper.getAccount().getAccountIdentity())
							|| accountHelper.hasPermissions(disc.getSubject().toDocument().getGroup()
									.getId(), AccountPermission.CAN_CHANGE_OTHER_MESSAGES),
					"no permission");*/

		Preconditions.checkNotNull(disc, "null discussion");
		Preconditions.checkNotNull(mess, "null message");
		Preconditions.checkNotNull(mess.getAuthor(), "null author identity");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(mess.getText()), "null message text");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(mess.getId()), "null message id");

		service.changeDiscussionMessage(id, subject, mess, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				Preconditions.checkArgument(result, "fail change message");

				// Put to cache
				disc.getMessage(mess.getId()).setText(mess.getText());

				// Fire an event
				eventBus.fireEvent(new DiscussionMessageReceivedEvent(mess, id, true));
				handler.onSuccess(true);
			}
		});
	}

	/**
	 * Put discussion to cache. All managers who have some discussion in his
	 * data MUST put it to cache of this manager.
	 * @param id TODO
	 * @param disc
	 */
	public void putDiscussionToCache(String id, Discussion disc) {
		discussionCache.put(id, disc);
	}

	@Override
	public void getDiscussion(String id, DiscussionSubject subject, AsyncCallback<Discussion> handler) {
		getDiscussion(id, null, 0, 30, handler);
	}

	@Override
	public void removeMessage(Discussion discussion, String id, DiscussionSubject subject,
			Message message, AsyncCallback<Boolean> asyncCallback) {
		
	}
}
